Unit tests are very useful for checking how our app is working.
Otherwise, we run into all kinds of issues later on.
In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.
Query HTML Elements Based on Attributes that are Unlikely to Change
We should only check for HTML elements with attributes that aren’t likely to change.
This way, we don’t have to update our tests when we make small changes.
Also, this ensures that look and feel changes don’t break our tests.
For example, instead of writing:
test("whenever no data is passed, number of messages shows zero", () => {
// ...
expect(wrapper.find("[className='mr-2']").text()).toBe("0");
});
to test:
<span id="metric" className="mr-2">{value}</span>
Instead, we add our own ID to the element that we want to test and then use that in our test.
For instance, if we have:
<h3>
<Badge pill className="fixed_badge" variant="dark">
<span data-testid="msgs-label">{value}</span>
</Badge>
</h3>
We can test it with:
test("whenever no data is passed, number of messages shows zero", () => {
const metricValue = undefined;
const { getByTestId } = render(<dashboardMetric value={undefined} />);
expect(getByTestId("msgs-label").text()).toBe("0");
});
We shouldn’t rely on CSS attributes that can change at any time.
Instead, we add an ID that rarely or never changes.
Test with a Realistic and Fully Rendered Component
We should test with realistic and fully rendered components.
This way, we can trust our test that it’s actually testing the stuff in the component.
If we mock or do partial or shallow rendering, we may miss things in our tests.
If it’s too slow to test with the real thing, then we can consider mocks.
For instance, instead of shallow rendering with shallow
:
test("when click to show filters, filters are displated", () => {
const wrapper = shallow(<Calendar showFilters={false} title="Select Filter" />);
wrapper
.find("FiltersPanel")
.instance()
.showFilters();
expect(wrapper.find("Filter").props()).toEqual({ title: "Select Filter" });
});
We write:
test("when click to show filters, filters are displated", () => {
const wrapper = mount(<Calendar showFilters={false} title="Select Filter" />);
wrapper.find("button").simulate("click");
expect(wrapper.text().includes("Select Filter"));
});
We call mount
to mount the Calendar
component fully.
Then we do the click on the button as we do like a real user.
Then we check the text that should appear.
Use Frameworks Built-in Support for Async Events
We should test frameworks built-in async events when we’re running our tests.
This way, we actually wait for what we want to appear before running something.
Sleeping for a fixed time isn’t reliable and doesn’t help with waiting for items to appear before doing what we want.
This means our tests would be flaky.
Also, sleeping for a fixed time is a lot slower.
For instance, with Cypress, we can write:
cy.get("#show-orders").click();
cy.wait("@orders");
We wait for orders
to appear when we click on the element with ID show-orders
.
What we don’t want is to have code that waits with our own logic with setInterval
:
test("user name appears", async () => {
//...
const interval = setInterval(() => {
const found = getByText("james");
if (found) {
clearInterval(interval);
expect(getByText("james")).toBeInTheDocument();
}
}, 100);
const movie = await waitForElement(() => getByText("james"));
});
This is complex and we don’t take advantage of the full capabilities of test frameworks.
Conclusion
We should wait for things with test frameworks’ wait functions.
Also, we should test with realistic components.